//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkAstraeus
#include <NilkinsAstraeus/Graphics/Materials/Utils/PbsEnvironmentMapFilter.h>

#include <NilkinsAstraeus/Graphics/Materials/Pbs/PbsMaterial.h>

#include <NilkinsAstraeus/Graphics/Materials/MaterialManager.h>

#include <NilkinsAstraeus/Log/LogManager.h>

#include <NilkinsAstraeus/Scripts/ScriptsEngine.h>

#include <NilkinsAstraeus/System.h>

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkGraphics
#include <NilkinsGraphics/Cameras/Camera.h>
#include <NilkinsGraphics/Cameras/CameraManager.h>

#include <NilkinsGraphics/Compositors/Compositor.h>
#include <NilkinsGraphics/Compositors/CompositorManager.h>
#include <NilkinsGraphics/Compositors/CompositorNode.h>
#include <NilkinsGraphics/Compositors/TargetOperations.h>

#include <NilkinsGraphics/Encoders/Obj/ObjEncoder.h>

#include <NilkinsGraphics/Entities/Entity.h>

#include <NilkinsGraphics/Graph/Node.h>
#include <NilkinsGraphics/Graph/NodeManager.h>

#include <NilkinsGraphics/Log/LogManager.h>

#include <NilkinsGraphics/Meshes/Utils/MeshUtils.h>

#include <NilkinsGraphics/Meshes/Mesh.h>
#include <NilkinsGraphics/Meshes/MeshManager.h>

#include <NilkinsGraphics/Passes/PostProcessPass.h>

#include <NilkinsGraphics/Programs/Program.h>
#include <NilkinsGraphics/Programs/ProgramManager.h>
#include <NilkinsGraphics/Programs/ProgramSourcesHolder.h>

#include <NilkinsGraphics/RenderContexts/RenderContextDescriptor.h>
#include <NilkinsGraphics/RenderContexts/RenderContextManager.h>

#include <NilkinsGraphics/RenderQueues/RenderQueue.h>
#include <NilkinsGraphics/RenderQueues/RenderQueueManager.h>

#include <NilkinsGraphics/Shaders/Memory/ConstantBuffer.h>
#include <NilkinsGraphics/Shaders/Memory/ShaderPassMemorySlot.h>

#include <NilkinsGraphics/Shaders/Shader.h>
#include <NilkinsGraphics/Shaders/ShaderManager.h>

#include <NilkinsGraphics/System.h>

#include <NilkinsGraphics/Textures/Texture.h>
#include <NilkinsGraphics/Textures/TextureManager.h>
#include <NilkinsGraphics/Textures/TextureUtils.h>

// nkResources
#include <NilkinsResources/ResourceManager.h>

// nkScripts
#include <NilkinsScripts/Environments/Environment.h>

#include <NilkinsScripts/Interpreters/Interpreter.h>

#include <NilkinsScripts/Scripts/Script.h>
#include <NilkinsScripts/Scripts/ScriptManager.h>

// Standards
#include <memory>

/// Internals --------------------------------

void basicMaterialDemo ()
{
	// Allocate the material
	nkAstraeus::PbsMaterial* mat = (nkAstraeus::PbsMaterial*)nkAstraeus::MaterialManager::getInstance()->createOrRetrieve("mat", nkAstraeus::MATERIAL_TYPE::PBS) ;

	// Set it up through its settings
	mat->setMetalness(0.7f) ;
	mat->setRoughnessGlossiness(0.5f) ;
	mat->setEnvironmentColor(nkMaths::Vector(0.2f, 0.2f, 0.2f)) ;

	// And don't forget to load it so that it compiles the program and prepare the shader
	mat->load() ;
}

void prepareCamera ()
{
	nkGraphics::Camera* cam = nkGraphics::CameraManager::getInstance()->getActiveRenderCamera() ;
	cam->setPositionRelative(nkMaths::Vector(9.f, 0.f, -20.f)) ;
}

void prepareTextures ()
{
	// Prepare the environment map
	nkGraphics::Texture* envMap = nkGraphics::TextureManager::getInstance()->createOrRetrieve("ENV") ;
	envMap->setPath("Data/environment.hdr") ;
	envMap->load() ;

	// Compute dynamically the irradiance map
	nkGraphics::Texture* irrMap = nkGraphics::TextureManager::getInstance()->createOrRetrieve("IRR") ;
	nkAstraeus::System::getInstance()->getPbsEnvironmentMapFilter()->requestIrradianceComputing(envMap, irrMap) ;
}

void addMesh (nkGraphics::Mesh* mesh, const nkMaths::Vector& position, float roughness)
{
	// Prepare the material this mesh will have
	nkGraphics::Texture* envMap = nkGraphics::TextureManager::getInstance()->get("ENV") ;
	nkGraphics::Texture* irrMap = nkGraphics::TextureManager::getInstance()->get("IRR") ;

	std::string matName = "MAT_" + std::to_string(roughness) ;
	nkAstraeus::PbsMaterial* mat = (nkAstraeus::PbsMaterial*)nkAstraeus::MaterialManager::getInstance()->createOrRetrieve(matName, nkAstraeus::MATERIAL_TYPE::PBS) ;

	mat->setMetalness(1.f) ;
	mat->setRoughnessGlossiness(roughness) ;
	mat->setEnvironmentTexture(envMap) ;
	mat->setIrradianceTexture(irrMap) ;	

	mat->load() ;

	// Prepare the graph, one node sufficient
	static unsigned int counter = 0 ;
	nkGraphics::Node* node = nkGraphics::NodeManager::getInstance()->createOrRetrieve(std::to_string(counter++)) ;

	// Retrieve the queue we will use
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;

	// Declare the entity, to feed the rendering information
	nkGraphics::Entity* ent = rq->addEntity() ;

	// The material is loaded, we can retrieve its shader to fit in within the entity
	ent->setRenderInfo(nkGraphics::EntityRenderInfo(mesh, mat->getShader())) ;

	// Move node
	ent->setParentNode(node) ;
	node->setPositionAbsolute(position) ;
}

void prepareScene ()
{
	// Prepare the mesh
	nkMemory::String absPath = nkResources::ResourceManager::getInstance()->getAbsoluteFromWorkingDir("Data/sphere.obj") ;
	nkMemory::Buffer fileData = nkResources::ResourceManager::getInstance()->loadFileIntoMemory(absPath) ;

	nkGraphics::ObjDecodeOptions objOptions ;
	objOptions._invertUvY = true ;
	objOptions._invertWindingOrder = true ;
	nkGraphics::DecodedData objData = nkGraphics::ObjEncoder::decode(fileData, objOptions) ;

	nkGraphics::Mesh* sphere = nkGraphics::MeshManager::getInstance()->createOrRetrieve("TEST") ;
	nkGraphics::MeshUtils::fillMeshFromDecodedData(objData._meshData[0], sphere) ;

	// Make a line of spheres to test roughness
	const float posMultiplier = 2.f ;

	for (unsigned int x = 0 ; x < 10 ; x++)
		addMesh(sphere, nkMaths::Vector(x * posMultiplier, 0.f, 0.f), x * 0.1f) ;
}

void prepareCompositor ()
{
	// Prepare a compositor to align environment visually (spheres and background)
	nkGraphics::Compositor* compo = nkGraphics::CompositorManager::getInstance()->createOrRetrieve("Compo") ;
	nkGraphics::CompositorNode* compoNode = compo->addNode() ;
	nkGraphics::TargetOperations* targetOp = compoNode->addOperations() ;

	// Operation will be rendered to the context buffers
	targetOp->setToBackBuffer(true) ;
	targetOp->setToChainDepthBuffer(true) ;

	// First clear, then paint the scene containing the spheres
	targetOp->addClearTargetsPass() ;
	targetOp->addRenderScenePass() ;

	// Prepare program for post process
	nkGraphics::Program* prog = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("Env_prog") ;

	nkGraphics::ProgramSourcesHolder sources ;
	sources.setVertexMemory
	(
		R"eos(
		struct VertexInput
		{
			float4 position : POSITION ;
			uint vertexId : SV_VertexID ;
		} ;

		struct PixelInput
		{
			float4 position : SV_POSITION ;
			float4 camDir : CAMDIR ;
			float4 positionUV : UV ;
		} ;

		cbuffer constants
		{
			// Direction cam
			float4 camDir [4] ;
		}

		PixelInput main (VertexInput input)
		{
			PixelInput result ;

			result.position = input.position ;
			result.positionUV = input.position ;
			result.camDir = camDir[input.vertexId] ;

			return result ;
		}
		)eos"
	) ;
	sources.setPixelMemory
	(
		R"eos(
		struct PixelInput
		{
			float4 position : SV_POSITION ;
			float4 camDir : CAMDIR ;
			float4 positionUV : UV ;
		} ;

		Texture2D tex : register(t0) ;
		SamplerState samplerDefault : register (s0) ;

		float4 main (PixelInput input) : SV_TARGET
		{
			float3 d = normalize(input.camDir.xyz) ;

			// We have a spherical 2D environment map, we need to convert the 3D direction to the 2D space
			float2 uvs ;
			uvs.x = 0.5 + atan2(d.z, d.x) / (2 * 3.1415956) ;
			uvs.y = 0.5 - asin(d.y) / 3.1415956 ;

			return tex.SampleLevel(samplerDefault, uvs, 2.5) ;
		}
		)eos"
	) ;

	prog->setFromMemory(sources) ;
	prog->load() ;

	// Fit into shader
	nkGraphics::Shader* shader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("Env_shader") ;
	shader->setProgram(prog) ;

	nkGraphics::Texture* envMap = nkGraphics::TextureManager::getInstance()->get("ENV") ;
	shader->addTexture(envMap, 0) ;
	
	nkGraphics::ConstantBuffer* buffer = shader->addConstantBuffer(0) ;
	nkGraphics::ShaderPassMemorySlot* slot = buffer->addPassMemorySlot() ;
	slot->setAsCamCornersWorld() ;

	shader->load() ;

	// Append the post process pass and load to finalize the compositor
	nkGraphics::PostProcessPass* postPass = targetOp->addPostProcessPass() ;
	postPass->setBackProcess(true) ;
	postPass->setShader(shader) ;

	// With this call, the default compositor is the one we just created
	// This means that all context which compositor is not set will now use this compositor when rendering
	nkGraphics::CompositorManager::getInstance()->setDefaultCompositor(compo) ;
}

/// Function ---------------------------------

int main ()
{
	// Prepare the logger
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkAstraeus::LogManager::getInstance()->setReceiver(logger.get()) ;
	nkGraphics::LogManager::getInstance()->setReceiver(logger.get()) ;

	// Initialize the engine
	if (!nkAstraeus::System::getInstance()->initialize())
		return -1 ;

	// Preparation for the scene
	prepareCamera() ;
	prepareTextures() ;
	prepareScene() ;
	prepareCompositor() ;

	// Prepare and launch rendering
	nkGraphics::RenderContext* context = nkGraphics::RenderContextManager::getInstance()->createRenderContext(nkGraphics::RenderContextDescriptor(800, 600, false, true)) ;
	nkGraphics::System::getInstance()->run(context) ;

	// We're done !
	nkAstraeus::System::getInstance()->shutdown() ;

	return 0 ;
}